/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.dex.file;

import java.io.PrintWriter;
import java.util.Map;
import java.util.TreeMap;

import com.android.dx.dex.code.CatchHandlerList;
import com.android.dx.dex.code.CatchTable;
import com.android.dx.dex.code.DalvCode;
import com.android.dx.util.AnnotatedOutput;
import com.android.dx.util.ByteArrayAnnotatedOutput;
import com.android.dx.util.Hex;

/**
 * List of exception handlers (tuples of covered range, catch type, handler
 * address) for a particular piece of code. Instances of this class correspond
 * to a {@code try_item[]} and a {@code catch_handler_item[]}.
 */
public final class CatchStructs {

	/**
	 * the size of a {@code try_item}: a {@code uint} and two {@code ushort}s
	 */
	private static final int TRY_ITEM_WRITE_SIZE = 4 + (2 * 2);

	/** {@code non-null;} code that contains the catches */
	private final DalvCode code;

	/**
	 * {@code null-ok;} the underlying table; set in
	 * {@link #finishProcessingIfNecessary}
	 */
	private CatchTable table;

	/**
	 * {@code null-ok;} the encoded handler list, if calculated; set in
	 * {@link #encode}
	 */
	private byte[] encodedHandlers;

	/**
	 * length of the handlers header (encoded size), if known; used for
	 * annotation
	 */
	private int encodedHandlerHeaderSize;

	/**
	 * {@code null-ok;} map from handler lists to byte offsets, if calculated;
	 * set in {@link #encode}
	 */
	private TreeMap<CatchHandlerList, Integer> handlerOffsets;

	/**
	 * Constructs an instance.
	 * 
	 * @param code
	 *            {@code non-null;} code that contains the catches
	 */
	public CatchStructs(DalvCode code) {
		this.code = code;
		this.table = null;
		this.encodedHandlers = null;
		this.encodedHandlerHeaderSize = 0;
		this.handlerOffsets = null;
	}

	/**
	 * Finish processing the catches, if necessary.
	 */
	private void finishProcessingIfNecessary() {
		if (table == null) {
			table = code.getCatches();
		}
	}

	/**
	 * Gets the size of the tries list, in entries.
	 * 
	 * @return {@code >= 0;} the tries list size
	 */
	public int triesSize() {
		finishProcessingIfNecessary();
		return table.size();
	}

	/**
	 * Does a human-friendly dump of this instance.
	 * 
	 * @param out
	 *            {@code non-null;} where to dump
	 * @param prefix
	 *            {@code non-null;} prefix to attach to each line of output
	 */
	public void debugPrint(PrintWriter out, String prefix) {
		annotateEntries(prefix, out, null);
	}

	/**
	 * Encodes the handler lists.
	 * 
	 * @param file
	 *            {@code non-null;} file this instance is part of
	 */
	public void encode(DexFile file) {
		finishProcessingIfNecessary();

		TypeIdsSection typeIds = file.getTypeIds();
		int size = table.size();

		handlerOffsets = new TreeMap<CatchHandlerList, Integer>();

		/*
		 * First add a map entry for each unique list. The tree structure will
		 * ensure they are sorted when we reiterate later.
		 */
		for (int i = 0; i < size; i++) {
			handlerOffsets.put(table.get(i).getHandlers(), null);
		}

		if (handlerOffsets.size() > 65535) {
			throw new UnsupportedOperationException("too many catch handlers");
		}

		ByteArrayAnnotatedOutput out = new ByteArrayAnnotatedOutput();

		// Write out the handlers "header" consisting of its size in entries.
		encodedHandlerHeaderSize = out.writeUleb128(handlerOffsets.size());

		// Now write the lists out in order, noting the offset of each.
		for (Map.Entry<CatchHandlerList, Integer> mapping : handlerOffsets
				.entrySet()) {
			CatchHandlerList list = mapping.getKey();
			int listSize = list.size();
			boolean catchesAll = list.catchesAll();

			// Set the offset before we do any writing.
			mapping.setValue(out.getCursor());

			if (catchesAll) {
				// A size <= 0 means that the list ends with a catch-all.
				out.writeSleb128(-(listSize - 1));
				listSize--;
			} else {
				out.writeSleb128(listSize);
			}

			for (int i = 0; i < listSize; i++) {
				CatchHandlerList.Entry entry = list.get(i);
				out.writeUleb128(typeIds.indexOf(entry.getExceptionType()));
				out.writeUleb128(entry.getHandler());
			}

			if (catchesAll) {
				out.writeUleb128(list.get(listSize).getHandler());
			}
		}

		encodedHandlers = out.toByteArray();
	}

	/**
	 * Gets the write size of this instance, in bytes.
	 * 
	 * @return {@code >= 0;} the write size
	 */
	public int writeSize() {
		return (triesSize() * TRY_ITEM_WRITE_SIZE) + +encodedHandlers.length;
	}

	/**
	 * Writes this instance to the given stream.
	 * 
	 * @param file
	 *            {@code non-null;} file this instance is part of
	 * @param out
	 *            {@code non-null;} where to write to
	 */
	public void writeTo(DexFile file, AnnotatedOutput out) {
		finishProcessingIfNecessary();

		if (out.annotates()) {
			annotateEntries("  ", null, out);
		}

		int tableSize = table.size();
		for (int i = 0; i < tableSize; i++) {
			CatchTable.Entry one = table.get(i);
			int start = one.getStart();
			int end = one.getEnd();
			int insnCount = end - start;

			if (insnCount >= 65536) {
				throw new UnsupportedOperationException(
						"bogus exception range: " + Hex.u4(start) + ".."
								+ Hex.u4(end));
			}

			out.writeInt(start);
			out.writeShort(insnCount);
			out.writeShort(handlerOffsets.get(one.getHandlers()));
		}

		out.write(encodedHandlers);
	}

	/**
	 * Helper method to annotate or simply print the exception handlers. Only
	 * one of {@code printTo} or {@code annotateTo} should be non-null.
	 * 
	 * @param prefix
	 *            {@code non-null;} prefix for each line
	 * @param printTo
	 *            {@code null-ok;} where to print to
	 * @param annotateTo
	 *            {@code null-ok;} where to consume bytes and annotate to
	 */
	private void annotateEntries(String prefix, PrintWriter printTo,
			AnnotatedOutput annotateTo) {
		finishProcessingIfNecessary();

		boolean consume = (annotateTo != null);
		int amt1 = consume ? 6 : 0;
		int amt2 = consume ? 2 : 0;
		int size = table.size();
		String subPrefix = prefix + "  ";

		if (consume) {
			annotateTo.annotate(0, prefix + "tries:");
		} else {
			printTo.println(prefix + "tries:");
		}

		for (int i = 0; i < size; i++) {
			CatchTable.Entry entry = table.get(i);
			CatchHandlerList handlers = entry.getHandlers();
			String s1 = subPrefix + "try " + Hex.u2or4(entry.getStart()) + ".."
					+ Hex.u2or4(entry.getEnd());
			String s2 = handlers.toHuman(subPrefix, "");

			if (consume) {
				annotateTo.annotate(amt1, s1);
				annotateTo.annotate(amt2, s2);
			} else {
				printTo.println(s1);
				printTo.println(s2);
			}
		}

		if (!consume) {
			// Only emit the handler lists if we are consuming bytes.
			return;
		}

		annotateTo.annotate(0, prefix + "handlers:");
		annotateTo.annotate(encodedHandlerHeaderSize, subPrefix + "size: "
				+ Hex.u2(handlerOffsets.size()));

		int lastOffset = 0;
		CatchHandlerList lastList = null;

		for (Map.Entry<CatchHandlerList, Integer> mapping : handlerOffsets
				.entrySet()) {
			CatchHandlerList list = mapping.getKey();
			int offset = mapping.getValue();

			if (lastList != null) {
				annotateAndConsumeHandlers(lastList, lastOffset, offset
						- lastOffset, subPrefix, printTo, annotateTo);
			}

			lastList = list;
			lastOffset = offset;
		}

		annotateAndConsumeHandlers(lastList, lastOffset, encodedHandlers.length
				- lastOffset, subPrefix, printTo, annotateTo);
	}

	/**
	 * Helper for {@link #annotateEntries} to annotate a catch handler list
	 * while consuming it.
	 * 
	 * @param handlers
	 *            {@code non-null;} handlers to annotate
	 * @param offset
	 *            {@code >= 0;} the offset of this handler
	 * @param size
	 *            {@code >= 1;} the number of bytes the handlers consume
	 * @param prefix
	 *            {@code non-null;} prefix for each line
	 * @param printTo
	 *            {@code null-ok;} where to print to
	 * @param annotateTo
	 *            {@code non-null;} where to annotate to
	 */
	private static void annotateAndConsumeHandlers(CatchHandlerList handlers,
			int offset, int size, String prefix, PrintWriter printTo,
			AnnotatedOutput annotateTo) {
		String s = handlers.toHuman(prefix, Hex.u2(offset) + ": ");

		if (printTo != null) {
			printTo.println(s);
		}

		annotateTo.annotate(size, s);
	}
}
